package com.citrinus.feedme.db;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.location.Location;

import com.citrinus.feedme.list.RestaurantComparator;
import com.citrinus.feedme.places.Place;
import com.citrinus.feedme.places.PlacesHandler;
import com.google.android.gms.maps.model.LatLng;

public final class Database {
	private static final String TAG = Database.class.getSimpleName();
	private static final int DATABASE_VERSION = 1;
	public static final String DATABASE_NAME = "feedme.db";

	private SQLiteDatabase db;
	private DBHelper dbHelper;

	public Database(Context context) {
		dbHelper = new DBHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
	}

	/**
	 * Opens the database for writing if possible, otherwise it is opened for
	 * reading.
	 * 
	 * @throws SQLiteException
	 *             If the database cannot be opened for writing and reading.
	 */
	public void open() throws SQLiteException {
		try {
			db = dbHelper.getWritableDatabase();
		} catch (SQLiteException exception) {
			db = dbHelper.getReadableDatabase();
		}
	}

	/**
	 * Adds a radar to this Database.
	 * <p>
	 * This method is thread-safe.
	 * 
	 * @param radar
	 *            The radar to add.
	 */
	public synchronized void addPlace(Place place) {
		ContentValues values = new ContentValues();
		values.put(DBHelper.TABLE_PLACES_ID, place.getId());
		values.put(DBHelper.TABLE_PLACES_NAME, place.getName());
		values.put(DBHelper.TABLE_PLACES_REFERENCE, place.getReference());
		values.put(DBHelper.TABLE_PLACES_ICON, place.getIcon());
		values.put(DBHelper.TABLE_PLACES_VICINITY, place.getVicinity());
		values.put(DBHelper.TABLE_PLACES_LATITUDE, place.getGeometry().getLocation().getLat());
		values.put(DBHelper.TABLE_PLACES_LONGITUDE, place.getGeometry().getLocation().getLng());

		try {
			db.insertOrThrow(DBHelper.TABLE_PLACES, null, values);
		} catch (SQLException exception) {
			db.update(DBHelper.TABLE_PLACES, values, DBHelper.TABLE_PLACES_ID + "='" +  place.getId()+"'", null);
		}
	}

	/**
	 * Add a list of places to Database
	 * <p>
	 * This method is thread-safe.
	 * 
	 * @param radar
	 *            The radar to add.
	 */
	public synchronized void addPlaces(List<Place> places) {
		for (Place p : places) {
			addPlace(p);
		}
	}

	/**
	 * Retrieves all the radars from this Database.
	 * <p>
	 * This method is thread-safe.
	 * 
	 * @return A list of radars.
	 */
	public synchronized List<Place> getPlaces() {
		List<Place> places = new LinkedList<Place>();
		Cursor cursor = db.query(DBHelper.TABLE_PLACES, null, null, null, null, null, null);

		try {
			Place place;
			while (cursor.moveToNext()) {
				place = new Place();
				place.setId(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_ID)));
				place.setName(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_NAME)));
				place.setReference(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_REFERENCE)));
				place.setIcon(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_ICON)));
				place.setVicinity(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_VICINITY)));
				double latitude = cursor.getDouble(cursor.getColumnIndex(DBHelper.TABLE_PLACES_LATITUDE));
				double longitude = cursor.getDouble(cursor.getColumnIndex(DBHelper.TABLE_PLACES_LONGITUDE));
				place.setLatLng(latitude, longitude);
				places.add(place);
			}
		} finally {
			cursor.close();
		}

		return places;

	}

	/*
	 * This method gets all restaurants in a square mile centering on user.
	 * After that, when populating the List, discard every restaurant more than
	 * a mile apart from the user position
	 * 
	 * I could have done some some calculations on query time in order to obtain
	 * exactly the places in a perfect mile circle but that would be very time
	 * consuming because of the calculations involved.
	 * 
	 * This way it involves less calculation. It is pretty strait forward
	 */
	public synchronized List<Place> getNearPlaces(LatLng point) {
		List<Place> places = new LinkedList<Place>();

		final double mult = 1.1; // mult = 1.1; is more reliable

		LatLng sqr1 = calculateDerivedPosition(point, mult * PlacesHandler.RADIUS_DISTANCE_SHOW, 0);
		LatLng sqr2 = calculateDerivedPosition(point, mult * PlacesHandler.RADIUS_DISTANCE_SHOW, 90);
		LatLng sqr3 = calculateDerivedPosition(point, mult * PlacesHandler.RADIUS_DISTANCE_SHOW, 180);
		LatLng sqr4 = calculateDerivedPosition(point, mult * PlacesHandler.RADIUS_DISTANCE_SHOW, 270);

		String where = "latitude > " + sqr3.latitude + " And " + " latitude< " + sqr1.latitude + " And " + "longitude < " + sqr2.longitude + " And "
				+ "longitude > " + sqr4.longitude;

		Cursor cursor = db.query(DBHelper.TABLE_PLACES, null, where, null, null, null, null);

		try {
			Place place;
			while (cursor.moveToNext()) {
				double latitude = cursor.getDouble(cursor.getColumnIndex(DBHelper.TABLE_PLACES_LATITUDE));
				double longitude = cursor.getDouble(cursor.getColumnIndex(DBHelper.TABLE_PLACES_LONGITUDE));

				place = new Place();
				place.setId(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_ID)));
				place.setName(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_NAME)));
				place.setReference(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_REFERENCE)));
				place.setIcon(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_ICON)));
				place.setVicinity(cursor.getString(cursor.getColumnIndex(DBHelper.TABLE_PLACES_VICINITY)));
				place.setLatLng(latitude, longitude);
				place.setDistanceToUser(distanceToRestaurant(point, place.getGeometry().getLocation().getLatLng()));

				if (place.getDistanceToUserInYards() < PlacesHandler.RADIUS_DISTANCE_SHOW) {
					places.add(place);
				}
				
			}
		} finally {
			cursor.close();
		}
		// order by distance
		Collections.sort(places, new RestaurantComparator());
		return places;
	}

	private float distanceToRestaurant(LatLng point, LatLng placePoint) {
		float[] results = new float[3];
		Location.distanceBetween(point.latitude, point.longitude, placePoint.latitude, placePoint.longitude, results);
		return results[0];

	}

	/**
	 * Checks if the database is empty.
	 * 
	 * @return TRUE if the database is emtpy, otherwise FALSE.
	 */
	public boolean isEmpty() {
		Cursor cursor = db.rawQuery("SELECT * FROM " + DBHelper.TABLE_PLACES, null);
		boolean isEmpty = true;

		if (cursor.moveToFirst()) {
			isEmpty = false;
		}

		cursor.close();

		return isEmpty;
	}

	/**
	 * Calculate a coordinate range meters from the point and with a specific
	 * bearing
	 * 
	 * @param point
	 * @param range
	 * @param bearing
	 * @return
	 */
	private LatLng calculateDerivedPosition(LatLng point, double range, double bearing) {
		double EarthRadius = 6371000; // m

		double latA = Math.toRadians(point.latitude);
		double lonA = Math.toRadians(point.longitude);
		double angularDistance = range / EarthRadius;
		double trueCourse = Math.toRadians(bearing);

		double lat = Math.asin(Math.sin(latA) * Math.cos(angularDistance) + Math.cos(latA) * Math.sin(angularDistance) * Math.cos(trueCourse));

		double dlon = Math.atan2(Math.sin(trueCourse) * Math.sin(angularDistance) * Math.cos(latA), Math.cos(angularDistance) - Math.sin(latA) * Math.sin(lat));

		double lon = ((lonA + dlon + Math.PI) % (Math.PI * 2)) - Math.PI;

		lat = Math.toDegrees(lat);
		lon = Math.toDegrees(lon);
		return new LatLng(lat, lon);
	}

}
